Skip to content

fix(storage): durable note catalog with crash recovery - #14

Merged
markm39 merged 3 commits into
mainfrom
fix/durable-note-catalog
Aug 29, 2026
Merged

fix(storage): durable note catalog with crash recovery#14
markm39 merged 3 commits into
mainfrom
fix/durable-note-catalog

Conversation

@markm39

@markm39 markm39 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Problem

A customer's tablet froze and their entire library appeared deleted. Root cause: note bodies are written durably to disk, but the note index and all metadata lived only in AsyncStorage. On iOS, AsyncStorage keeps small values in a single manifest that can be lost wholesale when the app dies mid-write (freeze, jetsam OOM kill, forced reboot). A corrupt index silently parsed to an empty library, and the next note creation overwrote it permanently — while every note body file sat intact on disk, invisible to the app.

Three adjacent data-loss bugs were found and fixed in the same pass.

Fixes

  1. Durable catalog + recovery — notes/folders metadata now persists to Documents/notes-catalog.json with the same atomic tmp-verify-rename pattern bodies use, mirrored to AsyncStorage. On load the catalog reconciles against body files on disk: any orphaned body is recovered into the library (real metadata from the mirror when it survived; a 'Recovered note' stub with preview and PDF background otherwise). Notes pointing at lost folders move to the root instead of vanishing. This recovers the affected user's notes on app update.
  2. No empty-overwrite — an unreadable body no longer opens as an empty note (which autosave would write over the real file). The editor shows a recovery screen: Try again / Back to library / explicit Start blank.
  3. No dropped saves on exit — back navigation previously cancelled the pending debounced save; it now flushes first, with a Try again / Leave anyway / Stay dialog on failure. Pending saves also flush when the app backgrounds (timers don't fire in background). Metadata no longer advances when a body write fails.
  4. Race fixes — catalog mutations are serialized through a shared promise queue, fixing read-modify-write races between concurrent deletes/moves.

Verification

  • 14 new node --test cases including REPRO: tests simulating the exact failure (corrupt index after mid-write kill; total manifest loss) — all pass
  • Typecheck clean; Metro export bundles for iOS
  • E2E on iPad Pro 11 simulator: seeded two body files, deleted the catalog, corrupted the AsyncStorage manifest, relaunched — both notes recovered in the library and open in the editor
  • /simplify (4-agent review, findings applied) and /security (no findings) run this session

https://claude.ai/code/session_01EEVkk7pkngePErcAAQveCx

Customer report: a tablet freeze wiped their entire library. Root cause:
the note index and all metadata lived solely in AsyncStorage, whose
manifest can be lost wholesale when the app dies mid-write. A corrupt
index silently became an empty library, and the next note creation
overwrote it permanently, even though every note body file survived on
disk.

- Add a durable catalog (Documents/notes-catalog.json) as the source of
  truth for notes and folders, written with the same atomic
  tmp-verify-rename pattern bodies use, mirrored to AsyncStorage, and
  reconciled on load against body files on disk: any orphaned body is
  recovered into the library (mirror metadata preferred, stub title as
  fallback, preview/PDF background restored). Notes pointing at lost
  folders move to the root instead of vanishing.
- Never open an unreadable body as an empty note (autosave would then
  overwrite the real file): show a recovery screen with retry, back,
  and an explicit start-blank choice.
- Flush pending autosaves on back navigation (previously cancelled,
  silently dropping the last strokes) with a retry dialog on failure,
  and on app background (timers do not fire in background).
- Skip the metadata update when a body write fails so updatedAt and
  thumbnail never advance past what actually persisted.
- Serialize catalog mutations through a shared promise queue, fixing
  read-modify-write races between concurrent deletes/moves.

Verified: 14 new node tests including reproductions of the corrupt-index
and total-manifest-loss scenarios; E2E on iPad simulator recovering
seeded notes after deleting the catalog and corrupting the AsyncStorage
manifest.

Claude-Session: https://claude.ai/code/session_01EEVkk7pkngePErcAAQveCx
Build 8 was prepared on chore/release-1.1-build-8; build 9 supersedes it
with the data-durability fix included.

Claude-Session: https://claude.ai/code/session_01EEVkk7pkngePErcAAQveCx
@markm39
markm39 force-pushed the fix/durable-note-catalog branch from df64a7e to 4e5da11 Compare August 29, 2026 05:25
@markm39
markm39 merged commit cb4babb into main Aug 29, 2026
1 check passed
@markm39
markm39 deleted the fix/durable-note-catalog branch August 29, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant